package IC.SemanticCheckes;

import IC.AST.*;
import IC.Types.*;
import IC.SymbolTable.*;
import java.util.*;
/** here we will implement a checker for semantic errors 
 * 
 *
 */

public class SemanticChecker implements Visitor {
	
	
	public SemanticChecker(IC.SymbolTable.GlobalSymbolTable global_st){
		this.global_st = global_st;
	}
	
	public Object visit(Program program) {
		// visit all classes
		for(ICClass c: program.getClasses()){
			c.accept(this);
		}
		return true;
	}

	
	public Object visit(ICClass icClass) {
		for(Method m: icClass.getMethods()){
			m.accept(this);
		}
		return true;
	}
	

	
	public Object methodGeneralVisit(Method method){
		
		for(Statement s: method.getStatements()){
			s.accept(this);
		}
		return true;
	}

	public Object visit(VirtualMethod method) {
		return methodGeneralVisit(method);
	}

	public Object visit(StaticMethod method) {
		processing_static_method = true;
		Object ret = methodGeneralVisit(method);
		processing_static_method = false;
		return ret;
	}

	public Object visit(LibraryMethod method) {
		return methodGeneralVisit(method);
	}

	
	public Object visit(Assignment assignment) {
		
		
		if (assignment.getVariable() instanceof VariableLocation) {
			VariableLocation var = (VariableLocation) assignment.getVariable();
			if (!var.isExternal()) {
				BlockSymbolTable st = (BlockSymbolTable) 
					assignment.getVariable().getEnclosingScope();
				
				st.getVarSymbol(var.getName(),var.getLine()).isInited = true;
			}
		}
		IC.Types.Type locationType = 
			(IC.Types.Type) assignment.getVariable().accept(this);
		
		IC.Types.Type assignmentType = 
			(IC.Types.Type) assignment.getAssignment().accept(this);
		
		// check subtyping <=
		
		if (!assignmentType.subtypeOf(locationType)){
			throw new SemanticError("trying assign incompatible values "+locationType.getName(),
						assignment.getLine(),
						assignmentType.getName());
		}
		
		return true;
	}

	public Object visit(CallStatement callStatement) {		
		/* check the call */
		callStatement.getCall().accept(this);
		return true;
	}

	public Object visit(Return returnStatement) {
		IC.Types.Type stmtRetVal = null; 
		IC.Types.Type methodRetVal = null;
		
		if (returnStatement.hasValue()){
			stmtRetVal = (IC.Types.Type) returnStatement.getValue().accept(this);
			// internal error
			if (stmtRetVal == null) {
				throw new InternalError("return value has no value object !");
			}
		} else try {
			/* only void */
			stmtRetVal = TypeTable.getType("void");
		} catch(SemanticError se)
			{assert(false);}
		
		/* lets see that the returned value is indeed the method returned value in the prototype */
		
		
			/* cast to block (in case of method extends block ) */
			methodRetVal = 
				((BlockSymbolTable) returnStatement.getEnclosingScope()).getVarSymbol("_ret",0).getType();
			
			/* specific for common case */
			if ( (methodRetVal == TypeTable.getType("void")) && 
			     (stmtRetVal != TypeTable.getType("void")) )
					throw new SemanticError("Void method should not return value",
							returnStatement.getLine(),
							stmtRetVal.getName());
			
			if (!methodRetVal.subtypeOf(stmtRetVal)){
					throw new SemanticError("method should return "+methodRetVal.getName(),
							returnStatement.getLine(),
							stmtRetVal.getName());
			}
		
		
		return true;
	}

	
	
	public Object visit(If ifStatement) {
		
		IC.Types.Type conditionType =
			(IC.Types.Type) ifStatement.getCondition().accept(this);
		if (conditionType == null)
			throw new InternalError("Condition null");
		
		/* check type of condition */
		
		if (!conditionType.subtypeOf(TypeTable.getType("boolean"))){
				throw new SemanticError("if with no boolean condition",
						ifStatement.getCondition().getLine(),
						conditionType.getName());
			}
		
		
		ifStatement.getOperation().accept(this);
		
		if (ifStatement.hasElse()){
			ifStatement.getElseOperation().accept(this);
		}
		
		return true;
	}

	public Object visit(VariableLocation location) {
		
		if (!location.isExternal()) {
			try {
				IC.Types.Type thisLocationType = 
					((BlockSymbolTable) location.getEnclosingScope()).getVarSymbol(location.getName(),location.getLine()).getType();
				if (! 
					((BlockSymbolTable) location.getEnclosingScope()).getVarSymbol(location.getName(),location.getLine()).isInited)
				{System.err.println("Warning: Variable "+location.getName()+
						" might have not been initizalied " + location.getLine());
					}
				return thisLocationType;
			} catch (SemanticError se) {
				se.setLine(location.getLine());
				throw se;
				
			}
		} else {
			/* external location */

			IC.Types.Type locationType = 
				(IC.Types.Type) location.getLocation().accept(this);
			
			if (locationType == null) 
				throw new InternalError("External location with null location");
			
			// handle case of class 
			try {
				TypeTable.getClassType(locationType.getName());
			} catch (SemanticError se) {
				throw new SemanticError("Accessing field of non-class/undefined type",
										location.getLocation().getLine(),
										locationType.getName());
			}
			try {
				IC.SymbolTable.ClassSymbolTable cst = 
					this.global_st.getClassSymbolTable(locationType.getName());
				IC.SymbolTable.VarSymbol fs = 
					cst.getFieldSymbol(location.getName());
				return fs.getType();
				
			} catch (SemanticError se) {
				
				throw new SemanticError("Accessing unexisting field of class",
						location.getLine(),
						location.getName());
			}
			
		}
	}

	
	public Object visit(While whileStatement) {
		
		IC.Types.Type conditionType = 
			(IC.Types.Type) whileStatement.getCondition().accept(this);
		if (conditionType == null) 
			throw new InternalError("While with null condition");
		
		
		if (!conditionType.subtypeOf(TypeTable.getType("boolean"))){
			throw new SemanticError("Condition in while is not of type boolean",
									whileStatement.getCondition().getLine(),
									conditionType.getName());
		}
		
		// check operation recursively
		loop_count++;
		whileStatement.getOperation().accept(this);
		loop_count--;
		
		return true;
	}
	
	public Object visit(ArrayLocation location) {
		// check if we accessing an array
		IC.Types.Type type = (IC.Types.Type) location.getArray().accept(this);
		if (!(type instanceof IC.Types.ArrayType))
			throw new SemanticError("Accessing with [] a non array type",location.getLine(),
									type.getName());
		
		// safe to convert to array
		IC.Types.ArrayType arrayType = (IC.Types.ArrayType) type;
		
		// check index
		IC.Types.Type indexType = (IC.Types.Type) location.getIndex().accept(this);
		
		// type check
		// check that index is of type int
		
		if (!indexType.subtypeOf(TypeTable.getType("int"))){
			throw new SemanticError("Array index must be of type int, type is",
					location.getLine(),
					indexType.getName());
		}
		
		return arrayType.getElemType();
		
	}

	public Object visit(LogicalBinaryOp binaryOp) {
		
		// check the two operands and get their types
		
		IC.Types.Type op1t = 
			(IC.Types.Type) binaryOp.getFirstOperand().accept(this);
		IC.Types.Type op2t = 
			(IC.Types.Type) binaryOp.getSecondOperand().accept(this);
		
		IC.BinaryOps op = binaryOp.getOperator();
		
		switch (op) {
		case LAND:
		case LOR:
			if ( (!op1t.subtypeOf(TypeTable.getType("boolean"))) ||
					(!op2t.subtypeOf(TypeTable.getType("boolean"))) )
					throw new SemanticError("Logical operations must be on booleans",
											binaryOp.getLine(),
											binaryOp.getOperator().getOperatorString());
			break;
		case EQUAL:
		case NEQUAL:
			if (!op1t.subtypeOf(op2t) && !op2t.subtypeOf(op1t)){
				throw new SemanticError("== and != can be used only on same/subtyped types",
						binaryOp.getLine(),
						binaryOp.getOperator().getOperatorString());
			}
			break;
		default:
			// inits
			if ( (!op1t.subtypeOf(TypeTable.getType("int"))) ||
					(!op2t.subtypeOf(TypeTable.getType("int"))) )
					throw new SemanticError("Arithmetic operations must be on integers",
											binaryOp.getLine(),
											op.getDescription());
			
		}
		return TypeTable.getType("boolean");
	
	}

	public Object visit(Break breakStatement) {
		if (loop_count == 0){
			throw new SemanticError("'break' statement not in loop",
					breakStatement.getLine(),
					"break");
		}
		
		return true;
	}

	
	public Object visit(Continue continueStatement) {
		if (loop_count == 0){
			throw new SemanticError("'continue' statement not in loop",
					continueStatement.getLine(),
					"continue");
		}

		return true;
	}

	
	public Object visit(StatementsBlock statementsBlock) {
		
		for(Statement s: statementsBlock.getStatements()){
			s.accept(this);
		}
		return true;
	}

	
	public Object visit(LocalVariable localVariable) {
		
		// check the init value if such
		if (localVariable.hasInitValue()) {
			IC.Types.Type initValueType = 
				(IC.Types.Type) localVariable.getInitValue().accept(this);
			
		
				IC.Types.Type localVariableType =
					((BlockSymbolTable) localVariable.getEnclosingScope()).getVarSymbol(localVariable.getName(),0).getType();
			
				if (!initValueType.subtypeOf(localVariableType)){
					throw new SemanticError("init value is not of defined type "+localVariableType.getName(),
											localVariable.getLine(),
											initValueType.getName());
				}
		} else {
			BlockSymbolTable st = (BlockSymbolTable) 
				localVariable.getEnclosingScope();
			st.getVarSymbol(localVariable.getName(),0).isInited = false;
		}
		
		return true;
	}

	
	
	
	public Object visit(StaticCall call) {
		// check if the class in the static call exists
		IC.SymbolTable.ClassSymbolTable cst = global_st.getClassSymbolTable(call.getClassName());
		if (cst == null){ // class does not exist
			throw new SemanticError("Cannot find class",
					call.getLine(),
					call.getClassName());
		}
		// check that the method is defined (as static) in enclosing class
		try{
			IC.SymbolTable.MethodSymbol ms = cst.getMethodSymbol(call.getName());
			
			if (!ms.isStatic()){
				throw new SemanticError("Method is not static",
						call.getLine(),
						call.getName());
			}
			
			Iterator<IC.Types.Type> methodArgsTypeIter =
				((IC.Types.MethodType) ms.getType()).getParamTypes().iterator();
			
			for(Expression arg: call.getArguments()){
				IC.Types.Type argType = 
					(IC.Types.Type) arg.accept(this);
				
				if (!argType.subtypeOf(methodArgsTypeIter.next())) {
					throw new SemanticError("type mismatched in argument passed to method",
							call.getLine(),
							argType.getName());
				}
			}

			if (methodArgsTypeIter.hasNext()){
				throw new SemanticError("not enough arguments passed to method",
						call.getLine(),
						call.getName());
			}
			
			return ((IC.Types.MethodType)
					ms.getType()).getReturnType();
		}catch (SemanticError se) {
			throw se;
		}catch (NoSuchElementException nsee){ 
			throw new SemanticError("Too many arguments passed to method",
					call.getLine(),
					call.getName());
		}
	}

	public Object visit(VirtualCall call) {
		
		IC.SymbolTable.ClassSymbolTable cst = null;
		
		if (call.isExternal()) {
			IC.Types.Type locType = 
				(IC.Types.Type) call.getLocation().accept(this); 
			
			cst = global_st.getClassSymbolTable(locType.getName());
			if (cst == null){
				throw new SemanticError("accessing not a user defined type",
						call.getLine(),
						locType.getName());
			}
		} else { 
			cst = ((BlockSymbolTable)call.getEnclosingScope()).getEnclosingClassSymbolTable();
			if (processing_static_method) {
				throw new SemanticError("Calling a virtual method from static is not allowed",
						call.getLine(),
						call.getName());
			}
		}
		
		MethodSymbol ms = null;
		try{
			ms = cst.getMethodSymbol(call.getName());
		} catch (SemanticError se) {// When method name is invalid.
			se.setLine(call.getLine());
			throw se;
		}
		
		if (ms.isStatic()){
			throw new SemanticError("Static method is called virtually",call.getLine(),call.getName());
		}
		// otherwise (method exists in class and is virtual) check arguments types
		Iterator<IC.Types.Type> methodArgsTypeIter = ((IC.Types.MethodType) ms.getType()).getParamTypes().iterator();
		for(Expression arg: call.getArguments()){
			IC.Types.Type argType = (IC.Types.Type) arg.accept(this);
			try{
				if (!argType.subtypeOf(methodArgsTypeIter.next())){ // wrong argument type sent to method
					throw new SemanticError("Wrong argument type passed to method",call.getLine(),argType.getName());
				}
			}catch (NoSuchElementException nsee){
				throw new SemanticError("Too many arguments passed to method",call.getLine(),call.getName());
			}
		}
		// check if method expects more parameters
		if (methodArgsTypeIter.hasNext()){
			throw new SemanticError("Too few arguments passed to method",call.getLine(),call.getName());
		}
		// Finally if got here, return the method's return type
		///////////////////////////////////////////////////////
		return ((IC.Types.MethodType) ms.getType()).getReturnType();
	}

	
	
	/**
	 * a Visitor for 'this' expression
	 * checks that it is not referenced inside a static method.
	 */
	public Object visit(This thisExpression) {
		if (processing_static_method) {
			throw new SemanticError("'this' referenced in static method",thisExpression.getLine(),"this");
		}
		return ((BlockSymbolTable) thisExpression.getEnclosingScope()).getEnclosingClassSymbolTable().getMySymbol().getType();
	}

	
	/**
	 * a Visitor for the newClass expression
	 * checks that the class type exists
	 */
	public Object visit(NewClass newClass) {
		IC.Types.ClassType ct = null;
		try{
			ct = IC.Types.TypeTable.getClassType(newClass.getName());
		}catch (SemanticError se){ // No such class exists
			se.setLine(newClass.getLine());
			throw se;
		}
		
		return ct;
	}

	
	/**
	 * a Visitor for NewArray expression.
	 * checks that elem type is a legal type.
	 * checks that size is of int type.
	 * returns the arrayType.
	 */
	public Object visit(NewArray newArray) {
		IC.Types.Type elemType = null;
		
		try {
			elemType = TypeTable.getType(newArray.getType().getBracketName());
		}catch (SemanticError se){ // illegal array elem type
			se.setLine(newArray.getLine());
			throw se;
		}
		
		IC.Types.Type sizeType = (IC.Types.Type) newArray.getSize().accept(this);
		
		if (!sizeType.subtypeOf(TypeTable.getType("int"))){
			throw new SemanticError("Array size not of int type",newArray.getLine(),sizeType.getName());
		}	
		return TypeTable.getType(elemType.getName()+"[]");
	}

	
	/**
	 * a Visitor for the array.length type.
	 * checks that array is an array.
	 * returns the type int.
	 */
	public Object visit(Length length) {
		IC.Types.Type arrType = (IC.Types.Type) length.getArray().accept(this);
		
		
		if (!arrType.getName().endsWith("[]")){ // not array type.
			throw new SemanticError("Not of array type",length.getLine(),arrType.getName());			
		}
				
		return TypeTable.getType("int");
	}

	
	/**
	 * a Visitor for MathBinaryOp
	 * checks that types are legal (int or string for +, int for everything else)
	 * returns the type of the operation.
	 */
	public Object visit(MathBinaryOp binaryOp) {
		IC.Types.Type op1Type = (IC.Types.Type) binaryOp.getFirstOperand().accept(this);
		IC.Types.Type op2Type = (IC.Types.Type) binaryOp.getSecondOperand().accept(this);
		if (op1Type != op2Type){ // check that both operands are of the same type
			throw new SemanticError("Math operation on different types",
					binaryOp.getLine(),
					binaryOp.getOperator().getOperatorString());
		}

		if (binaryOp.getOperator() != IC.BinaryOps.PLUS){// operator is one of "-","*","/","%"			

			if (!op1Type.subtypeOf(TypeTable.getType("int"))){// enough to check only one of the operands' type, since they are of the same type
				throw new SemanticError("Math operation on a non int type",binaryOp.getLine(),op1Type.getName());
		}
		}else{
			if (!op1Type.subtypeOf(TypeTable.getType("int")) && !op1Type.subtypeOf(TypeTable.getType("string"))){
				throw new SemanticError("+ operation on an illegal type",binaryOp.getLine(),op1Type.getName());
			}
		}
		// Legal types
		return op1Type;
	}

	
	
	
	/**
	 * a Visitor for MathUnaryOp - only one math unary operation - unary minus. 
	 * checks that the operand is of type int.
	 * returns type int.
	 */
	public Object visit(MathUnaryOp unaryOp) {
		IC.Types.Type opType = (IC.Types.Type) unaryOp.getOperand().accept(this);

		if (!opType.subtypeOf(TypeTable.getType("int"))){// opType is not an integer
			throw new SemanticError("Mathematical unary operation on a non int type",unaryOp.getLine(),opType.getName());
		}
		return opType; // int
	}

	
	/**
	 * a Visitor for LogicalUnaryOp - only one logic unary operation - unary logical negation. 
	 * checks that the operand is of type boolean.
	 * returns type boolean.
	 */
	public Object visit(LogicalUnaryOp unaryOp) {
		IC.Types.Type opType = (IC.Types.Type) unaryOp.getOperand().accept(this);
		if (!opType.subtypeOf(TypeTable.getType("boolean"))){// opType is not a boolean
			throw new SemanticError("Logical unary operation on a non boolean type",unaryOp.getLine(),opType.getName());
		}
		return opType; // boolean
	}

	
	/**
	 * Literal visitor:
	 * returns the type of the literal
	 */
	public Object visit(Literal literal) {
		IC.LiteralTypes type = literal.getType();

		// return the corresponding type of the literal
		switch (type){
			case STRING: 
				return TypeTable.getType("string");
			case INTEGER: 
				return TypeTable.getType("int");
			case TRUE: 
				return TypeTable.getType("boolean");
			case FALSE: 
				return TypeTable.getType("boolean");
			case NULL: 
				return TypeTable.getType("null");
		}
		
		return null;
	}

	
	/**
	 * ExpressionBlock visitor:
	 * - recursive call to expression
	 * returns null if encountered an error, and the type of the expression otherwise 
	 */
	public Object visit(ExpressionBlock expressionBlock) {
		return (IC.Types.Type) expressionBlock.getExpression().accept(this);  // will return null if encounters an error
	}


	@Override
	public Object visit(FieldOrMethodList field_or_method) {
		return null;
	}
	/** stubs 
	 * 
	 */
	public Object visit(Field field) {
		return true;
	}
	public Object visit(Formal formal) {
		return true;
	}

	
	public Object visit(PrimitiveType type) {
		return true;
	}

	
	public Object visit(UserType type) {
		return true;
	}

	/* members of class */
	private IC.SymbolTable.GlobalSymbolTable global_st;
	private boolean processing_static_method = false;
	private int loop_count = 0;
}
